// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])

@TestOn('!chrome')
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import '../widgets/clipboard_utils.dart';
import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
import '../widgets/semantics_tester.dart';

class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
  @override
  bool isSupported(Locale locale) => true;

  @override
  Future<MaterialLocalizations> load(Locale locale) => DefaultMaterialLocalizations.load(locale);

  @override
  bool shouldReload(MaterialLocalizationsDelegate old) => false;
}

class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
  @override
  bool isSupported(Locale locale) => true;

  @override
  Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);

  @override
  bool shouldReload(WidgetsLocalizationsDelegate old) => false;
}

Widget overlay({ Widget? child }) {
  final OverlayEntry entry = OverlayEntry(
    builder: (BuildContext context) {
      return Center(
        child: Material(
          child: child,
        ),
      );
    },
  );
  return overlayWithEntry(entry);
}

Widget overlayWithEntry(OverlayEntry entry) {
  return Localizations(
    locale: const Locale('en', 'US'),
    delegates: <LocalizationsDelegate<dynamic>>[
      WidgetsLocalizationsDelegate(),
      MaterialLocalizationsDelegate(),
    ],
    child: Directionality(
      textDirection: TextDirection.ltr,
      child: MediaQuery(
        data: const MediaQueryData(size: Size(800.0, 600.0)),
        child: Overlay(
          initialEntries: <OverlayEntry>[
            entry,
          ],
        ),
      ),
    ),
  );
}

Widget boilerplate({ Widget? child }) {
  return Localizations(
    locale: const Locale('en', 'US'),
    delegates: <LocalizationsDelegate<dynamic>>[
      WidgetsLocalizationsDelegate(),
      MaterialLocalizationsDelegate(),
    ],
    child: Directionality(
      textDirection: TextDirection.ltr,
      child: MediaQuery(
        data: const MediaQueryData(size: Size(800.0, 600.0)),
        child: Center(
          child: Material(
            child: child,
          ),
        ),
      ),
    ),
  );
}

Future<void> skipPastScrollingAnimation(WidgetTester tester) async {
  await tester.pump();
  await tester.pump(const Duration(milliseconds: 200));
}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  final MockClipboard mockClipboard = MockClipboard();

  const String kThreeLines =
      'First line of text is\n'
      'Second line goes until\n'
      'Third line of stuff';
  const String kMoreThanFourLines =
      '$kThreeLines\n'
      "Fourth line won't display and ends at";

  // Returns the first RenderEditable.
  RenderEditable findRenderEditable(WidgetTester tester) {
    final RenderObject root = tester.renderObject(find.byType(EditableText));
    expect(root, isNotNull);

    late RenderEditable renderEditable;
    void recursiveFinder(RenderObject child) {
      if (child is RenderEditable) {
        renderEditable = child;
        return;
      }
      child.visitChildren(recursiveFinder);
    }
    root.visitChildren(recursiveFinder);
    expect(renderEditable, isNotNull);
    return renderEditable;
  }

  List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
    return points.map<TextSelectionPoint>((TextSelectionPoint point) {
      return TextSelectionPoint(
        box.localToGlobal(point.point),
        point.direction,
      );
    }).toList();
  }

  setUp(() async {
    debugResetSemanticsIdCounter();
    TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
      SystemChannels.platform,
      mockClipboard.handleMethodCall,
    );
    // Fill the clipboard so that the Paste option is available in the text
    // selection menu.
    await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
  });

  tearDown(() {
    TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
      SystemChannels.platform,
      null,
    );
  });

  Widget selectableTextBuilder({
    String text = '',
    int? maxLines = 1,
    int? minLines,
  }) {
    return boilerplate(
      child: SelectableText(
        text,
        style: const TextStyle(color: Colors.black, fontSize: 34.0),
        maxLines: maxLines,
        minLines: minLines,
      ),
    );
  }

  testWidgets('throw if no Overlay widget exists above', (WidgetTester tester) async {
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: MediaQuery(
          data: MediaQueryData(size: Size(800.0, 600.0)),
          child: Center(
            child: Material(
              child: SelectableText('I love Flutter!'),
            ),
          ),
        ),
      ),
    );

    final Offset textFieldStart = tester.getTopLeft(find.byType(SelectableText));
    final TestGesture gesture = await tester.startGesture(textFieldStart, kind: PointerDeviceKind.mouse);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pumpAndSettle();

    final FlutterError error = tester.takeException() as FlutterError;
    expect(
      error.message,
      contains('EditableText widgets require an Overlay widget ancestor'),
    );

    await tester.pumpWidget(const SizedBox.shrink());
    expect(tester.takeException(), isNotNull);  // side effect exception
  });

  testWidgets('Do not crash when remove SelectableText during handle drag', (WidgetTester tester) async {
    // Regression test https://github.com/flutter/flutter/issues/108242
    bool isShow = true;
    late StateSetter setter;
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              setter = setState;
              if (isShow) {
                return const SelectableText(
                  'abc def ghi',
                  dragStartBehavior: DragStartBehavior.down,
                );
              } else {
                return const SizedBox.shrink();
              }
            },
          ),
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Long press the 'e' to select 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

    final TextSelection selection = controller.selection;
    expect(selection.baseOffset, 4);
    expect(selection.extentOffset, 7);

    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    // Drag the left handle to the left.
    final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
    final Offset newHandlePos = textOffsetToPosition(tester, 1);
    final Offset newHandlePos1 = textOffsetToPosition(tester, 0);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();

    // Unmount the SelectableText during handle drag
    setter(() {
      isShow = false;
    });
    await tester.pump();

    await gesture.moveTo(newHandlePos1);
    await tester.pump(); // Do not crash here

    await gesture.up();
    await tester.pump();
  });

  testWidgets('has expected defaults', (WidgetTester tester) async {
    await tester.pumpWidget(
      boilerplate(
          child: const SelectableText('selectable text'),
      ),
    );

    final SelectableText selectableText = tester.firstWidget(find.byType(SelectableText));
    expect(selectableText.showCursor, false);
    expect(selectableText.autofocus, false);
    expect(selectableText.dragStartBehavior, DragStartBehavior.start);
    expect(selectableText.cursorWidth, 2.0);
    expect(selectableText.cursorHeight, isNull);
    expect(selectableText.enableInteractiveSelection, true);
  });

  testWidgets('Rich selectable text has expected defaults', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MediaQuery(
        data: MediaQueryData(),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: SelectableText.rich(
              TextSpan(
                text: 'First line!',
                style: TextStyle(
                    fontSize: 14,
                    fontFamily: 'Roboto',
                ),
                children: <TextSpan>[
                  TextSpan(
                    text: 'Second line!\n',
                    style: TextStyle(
                      fontSize: 30,
                      fontFamily: 'Roboto',
                    ),
                  ),
                  TextSpan(
                    text: 'Third line!\n',
                    style: TextStyle(
                      fontSize: 14,
                      fontFamily: 'Roboto',
                    ),
                  ),
                ],
              ),
          ),
        ),
      ),
    );

    final SelectableText selectableText =
    tester.firstWidget(find.byType(SelectableText));
    expect(selectableText.showCursor, false);
    expect(selectableText.autofocus, false);
    expect(selectableText.dragStartBehavior, DragStartBehavior.start);
    expect(selectableText.cursorWidth, 2.0);
    expect(selectableText.cursorHeight, isNull);
    expect(selectableText.enableInteractiveSelection, true);
  });

  testWidgets('Rich selectable text supports WidgetSpan', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MediaQuery(
        data: MediaQueryData(),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: SelectableText.rich(
              TextSpan(
                text: 'First line!',
                style: TextStyle(
                    fontSize: 14,
                    fontFamily: 'Roboto',
                ),
                children: <InlineSpan>[
                  WidgetSpan(
                      child: SizedBox(
                        width: 120,
                        height: 50,
                        child: Card(
                            child: Center(
                                child: Text('Hello World!'),
                            ),
                        ),
                      ),
                  ),
                  TextSpan(
                    text: 'Third line!\n',
                    style: TextStyle(
                      fontSize: 14,
                      fontFamily: 'Roboto',
                    ),
                  ),
                ],
              ),
          ),
        ),
      ),
    );
    expect(tester.takeException(), isNull);
  });

  testWidgets('no text keyboard when widget is focused', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText('selectable text'),
        ),
    );
    await tester.tap(find.byType(SelectableText));
    await tester.idle();
    expect(tester.testTextInput.hasAnyClients, false);
  });

  testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async {
    const Color selectionColor = Colors.orange;
    const Color cursorColor = Colors.red;

    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: DefaultSelectionStyle(
            selectionColor: selectionColor,
            cursorColor: cursorColor,
            child: SelectableText('text'),
          ),
        ),
      ),
    );
    await tester.pump();
    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
    expect(state.widget.selectionColor, selectionColor);
    expect(state.widget.cursorColor, cursorColor);
  });

  testWidgets('Selectable Text has adaptive size', (WidgetTester tester) async {
    await tester.pumpWidget(
        boilerplate(
          child: const SelectableText('s'),
        ),
    );

    RenderBox findSelectableTextBox() => tester.renderObject(find.byType(SelectableText));

    final RenderBox textBox = findSelectableTextBox();
    expect(textBox.size, const Size(17.0, 14.0));

    await tester.pumpWidget(
        boilerplate(
          child: const SelectableText('very very long'),
        ),
    );

    final RenderBox longtextBox = findSelectableTextBox();
    expect(longtextBox.size, const Size(199.0, 14.0));
  });

  testWidgets('can scale with textScaleFactor', (WidgetTester tester) async {
    await tester.pumpWidget(
      boilerplate(
        child: const SelectableText('selectable text'),
      ),
    );

    final RenderBox renderBox = tester.renderObject(find.byType(SelectableText));
    expect(renderBox.size.height, 14.0);

    await tester.pumpWidget(
      boilerplate(
        child: const SelectableText(
          'selectable text',
          textScaleFactor: 1.9,
        ),
      ),
    );

    final RenderBox scaledBox = tester.renderObject(find.byType(SelectableText));
    expect(scaledBox.size.height, 27.0);
  });

  testWidgets('can switch between textWidthBasis', (WidgetTester tester) async {
    RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText));
    const String text = 'I can face roll keyboardkeyboardaszzaaaaszzaaaaszzaaaaszzaaaa';
    await tester.pumpWidget(
      boilerplate(
        child: const SelectableText(
          text,
          textWidthBasis: TextWidthBasis.parent,
        ),
      ),
    );
    RenderBox textBox = findTextBox();
    expect(textBox.size, const Size(800.0, 28.0));

    await tester.pumpWidget(
      boilerplate(
        child: const SelectableText(
          text,
          textWidthBasis: TextWidthBasis.longestLine,
        ),
      ),
    );
    textBox = findTextBox();
    expect(textBox.size, const Size(633.0, 28.0));
  });

  testWidgets('can switch between textHeightBehavior', (WidgetTester tester) async {
    const String text = 'selectable text';
    const TextHeightBehavior textHeightBehavior = TextHeightBehavior(
      applyHeightToFirstAscent: false,
      applyHeightToLastDescent: false,
    );
    await tester.pumpWidget(
      boilerplate(
        child: const SelectableText(text),
      ),
    );
    expect(findRenderEditable(tester).textHeightBehavior, isNull);

    await tester.pumpWidget(
      boilerplate(
        child: const SelectableText(
          text,
          textHeightBehavior: textHeightBehavior,
        ),
      ),
    );
    expect(findRenderEditable(tester).textHeightBehavior, textHeightBehavior);
  });

  testWidgets('Cursor blinks when showCursor is true', (WidgetTester tester) async {
    await tester.pumpWidget(
      overlay(
        child: const SelectableText(
          'some text',
          showCursor: true,
        ),
      ),
    );
    await tester.tap(find.byType(SelectableText));
    await tester.idle();

    final EditableTextState editableText = tester.state(find.byType(EditableText));

    // Check that the cursor visibility toggles after each blink interval.
    final bool initialShowCursor = editableText.cursorCurrentlyVisible;
    await tester.pump(editableText.cursorBlinkInterval);
    expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor));
    await tester.pump(editableText.cursorBlinkInterval);
    expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
    await tester.pump(editableText.cursorBlinkInterval ~/ 10);
    expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
    await tester.pump(editableText.cursorBlinkInterval);
    expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor));
    await tester.pump(editableText.cursorBlinkInterval);
    expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
  });

  testWidgets('selectable text selection toolbar renders correctly inside opacity', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(
          body: Center(
            child: SizedBox(
              width: 100,
              height: 100,
              child: Opacity(
                opacity: 0.5,
                child: SelectableText('selectable text'),
              ),
            ),
          ),
        ),
      ),
    );

    // The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
    state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);

    expect(state.showToolbar(), true);

    // This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible.
    await tester.pumpAndSettle();
    await tester.pump(const Duration(seconds: 1));

    expect(find.text('Select all'), findsOneWidget);
  });

  testWidgets('Caret position is updated on tap', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText('abc def ghi'),
        ),
    );
    final EditableText editableText = tester.widget(find.byType(EditableText));
    expect(editableText.controller.selection.baseOffset, -1);
    expect(editableText.controller.selection.extentOffset, -1);

    // Tap to reposition the caret.
    const int tapIndex = 4;
    final Offset ePos = textOffsetToPosition(tester, tapIndex);
    await tester.tapAt(ePos);
    await tester.pump();

    expect(editableText.controller.selection.baseOffset, tapIndex);
    expect(editableText.controller.selection.extentOffset, tapIndex);
  });

  testWidgets('enableInteractiveSelection = false, tap', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText(
            'abc def ghi',
            enableInteractiveSelection: false,
          ),
        ),
    );
    final EditableText editableText = tester.widget(find.byType(EditableText));
    expect(editableText.controller.selection.baseOffset, -1);
    expect(editableText.controller.selection.extentOffset, -1);

    // Tap would ordinarily reposition the caret.
    const int tapIndex = 4;
    final Offset ePos = textOffsetToPosition(tester, tapIndex);
    await tester.tapAt(ePos);
    await tester.pump();

    expect(editableText.controller.selection.baseOffset, -1);
    expect(editableText.controller.selection.extentOffset, -1);
  });

  testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText(
            'abc def ghi',
            enableInteractiveSelection: false,
          ),
        ),
    );
    final EditableText editableText = tester.widget(find.byType(EditableText));
    expect(editableText.controller.selection.baseOffset, -1);
    expect(editableText.controller.selection.extentOffset, -1);

    // Long press the 'e' to select 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();

    expect(editableText.controller.selection.isCollapsed, true);
    expect(editableText.controller.selection.baseOffset, -1);
    expect(editableText.controller.selection.extentOffset, -1);
  });

  testWidgets('Can long press to select', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText('abc def ghi'),
        ),
    );

    final EditableText editableText = tester.widget(find.byType(EditableText));

    expect(editableText.controller.selection.isCollapsed, true);

    // Long press the 'e' to select 'def'.
    const int tapIndex = 5;
    final Offset ePos = textOffsetToPosition(tester, tapIndex);
    await tester.longPressAt(ePos);
    await tester.pump();

    // 'def' is selected.
    expect(editableText.controller.selection.baseOffset, 4);
    expect(editableText.controller.selection.extentOffset, 7);

    // Tapping elsewhere immediately collapses and moves the cursor.
    await tester.tapAt(textOffsetToPosition(tester, 9));
    await tester.pump();

    expect(editableText.controller.selection.isCollapsed, true);
    expect(editableText.controller.selection.baseOffset, 9);
  });

  testWidgets("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText('abc def ghi'),
        ),
    );
    // Long press the 'e' to select 'def', but don't release the gesture.
    final Offset ePos = textOffsetToPosition(tester, 5);
    final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await tester.pumpAndSettle();

    // Handles are shown
    final Finder fadeFinder = find.byType(FadeTransition);
    expect(fadeFinder, findsNWidgets(2)); // 2 handles, 1 toolbar
    FadeTransition handle = tester.widget(fadeFinder.at(0));
    expect(handle.opacity.value, equals(1.0));

    // Move the gesture very slightly
    await gesture.moveBy(const Offset(1.0, 1.0));
    await tester.pump(SelectionOverlay.fadeDuration * 0.5);
    handle = tester.widget(fadeFinder.at(0));

    // The handle should still be fully opaque.
    expect(handle.opacity.value, equals(1.0));
  });

  testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText('abc def ghi'),
        ),
    );

    final EditableText editableText = tester.widget(find.byType(EditableText));

    // Long press the 'e' using a mouse device.
    const int eIndex = 5;
    final Offset ePos = textOffsetToPosition(tester, eIndex);
    final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();

    // The cursor is placed just like a regular tap.
    expect(editableText.controller.selection.baseOffset, eIndex);
    expect(editableText.controller.selection.extentOffset, eIndex);
  });

  testWidgets('selectable text basic', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText('selectable'),
        ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    // selectable text cannot open keyboard.
    await tester.showKeyboard(find.byType(SelectableText));
    expect(tester.testTextInput.hasAnyClients, false);
    await skipPastScrollingAnimation(tester);

    expect(editableTextWidget.controller.selection.isCollapsed, true);

    await tester.tap(find.byType(SelectableText));
    await tester.pump();

    final EditableTextState editableText = tester.state(find.byType(EditableText));
    // Collapse selection should not paint.
    expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
    // Long press on the 't' character of text 'selectable' to show context menu.
    const int dIndex = 5;
    final Offset dPos = textOffsetToPosition(tester, dIndex);
    await tester.longPressAt(dPos);
    await tester.pump();

    // Context menu should not have paste and cut.
    expect(find.text('Copy'), findsOneWidget);
    expect(find.text('Paste'), findsNothing);
    expect(find.text('Cut'), findsNothing);
  });

  testWidgets('selectable text can disable toolbar options', (WidgetTester tester) async {
    await tester.pumpWidget(
      overlay(
        child: const SelectableText(
          'a selectable text',
          toolbarOptions: ToolbarOptions(
            selectAll: true,
          ),
        ),
      ),
    );
    const int dIndex = 5;
    final Offset dPos = textOffsetToPosition(tester, dIndex);
    await tester.longPressAt(dPos);
    await tester.pump();
    // Context menu should not have copy.
    expect(find.text('Copy'), findsNothing);
    expect(find.text('Select all'), findsOneWidget);
  });

  testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            dragStartBehavior: DragStartBehavior.down,
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    final Offset ePos = textOffsetToPosition(tester, 5);
    final Offset gPos = textOffsetToPosition(tester, 8);

    final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
    await tester.pump();
    await gesture.moveTo(gPos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();

    expect(controller.selection.baseOffset, 5);
    expect(controller.selection.extentOffset, 8);
  });

  testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            dragStartBehavior: DragStartBehavior.down,
            style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    int selectionChangedCount = 0;

    controller.addListener(() {
      selectionChangedCount++;
    });

    final Offset cPos = textOffsetToPosition(tester, 2); // Index of 'c'.
    final Offset gPos = textOffsetToPosition(tester, 8); // Index of 'g'.
    final Offset hPos = textOffsetToPosition(tester, 9); // Index of 'h'.

    // Drag from 'c' to 'g'.
    final TestGesture gesture = await tester.startGesture(cPos, kind: PointerDeviceKind.mouse);
    await tester.pump();
    await gesture.moveTo(gPos);
    await tester.pumpAndSettle();

    expect(selectionChangedCount, isNonZero);
    selectionChangedCount = 0;
    expect(controller.selection.baseOffset, 2);
    expect(controller.selection.extentOffset, 8);

    // Tiny movement shouldn't cause text selection to change.
    await gesture.moveTo(gPos + const Offset(4.0, 0.0));
    await tester.pumpAndSettle();
    expect(selectionChangedCount, 0);

    // Now a text selection change will occur after a significant movement.
    await gesture.moveTo(hPos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();

    expect(selectionChangedCount, 1);
    expect(controller.selection.baseOffset, 2);
    expect(controller.selection.extentOffset, 9);
  });

  testWidgets('Dragging in opposite direction also works', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            dragStartBehavior: DragStartBehavior.down,
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    final Offset ePos = textOffsetToPosition(tester, 5);
    final Offset gPos = textOffsetToPosition(tester, 8);

    final TestGesture gesture = await tester.startGesture(gPos, kind: PointerDeviceKind.mouse);
    await tester.pump();
    await gesture.moveTo(ePos);
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();

    expect(controller.selection.baseOffset, 8);
    expect(controller.selection.extentOffset, 5);
  });

  testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            dragStartBehavior: DragStartBehavior.down,
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    final Offset ePos = textOffsetToPosition(tester, 5);
    final Offset gPos = textOffsetToPosition(tester,8);

    final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
    await tester.pump(const Duration(seconds: 2));
    await gesture.moveTo(gPos);
    await tester.pump();
    await gesture.up();

    expect(controller.selection.baseOffset, 5);
    expect(controller.selection.extentOffset,8);
  });

  testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            dragStartBehavior: DragStartBehavior.down,
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Long press the 'e' to select 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

    final TextSelection selection = controller.selection;
    expect(selection.baseOffset, 4);
    expect(selection.extentOffset, 7);

    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    // Drag the right handle 2 letters to the right.
    // We use a small offset because the endpoint is on the very corner
    // of the handle.
    Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    Offset newHandlePos = textOffsetToPosition(tester, 11);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(controller.selection.baseOffset, 4);
    expect(controller.selection.extentOffset, 11);

    // Drag the left handle 2 letters to the left.
    handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
    newHandlePos = textOffsetToPosition(tester, 0);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(controller.selection.baseOffset, 0);
    expect(controller.selection.extentOffset, 11);
  });

  testWidgets('Dragging handles calls onSelectionChanged', (WidgetTester tester) async {
    TextSelection? newSelection;
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            dragStartBehavior: DragStartBehavior.down,
            onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
              expect(newSelection, isNull);
              newSelection = selection;
            },
          ),
        ),
      ),
    );

    // Long press the 'e' to select 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

    expect(newSelection!.baseOffset, 4);
    expect(newSelection!.extentOffset, 7);

    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(newSelection!),
      renderEditable,
    );
    expect(endpoints.length, 2);
    newSelection = null;

    // Drag the right handle 2 letters to the right.
    // We use a small offset because the endpoint is on the very corner
    // of the handle.
    final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    final Offset newHandlePos = textOffsetToPosition(tester, 9);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(newSelection!.baseOffset, 4);
    expect(newSelection!.extentOffset, 9);
  });

  testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            dragStartBehavior: DragStartBehavior.down,
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Long press the 'e' to select 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5); // Position before 'e'.
    TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

    final TextSelection selection = controller.selection;
    expect(selection.baseOffset, 4);
    expect(selection.extentOffset, 7);

    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    // Drag the right handle until there's only 1 char selected.
    // We use a small offset because the endpoint is on the very corner
    // of the handle.
    final Offset handlePos = endpoints[1].point + const Offset(4.0, 0.0);
    Offset newHandlePos = textOffsetToPosition(tester, 5); // Position before 'e'.
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();

    expect(controller.selection.baseOffset, 4);
    expect(controller.selection.extentOffset, 5);

    newHandlePos = textOffsetToPosition(tester, 2); // Position before 'c'.
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(controller.selection.baseOffset, 4);
    // The selection doesn't move beyond the left handle. There's always at
    // least 1 char selected.
    expect(controller.selection.extentOffset, 5);
  });

  testWidgets('Can use selection toolbar', (WidgetTester tester) async {
    const String testValue = 'abc def ghi';
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(
            testValue,
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Tap the selection handle to bring up the "paste / select all" menu.
    await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    // Tapping on the part of the handle's GestureDetector where it overlaps
    // with the text itself does not show the menu, so add a small vertical
    // offset to tap below the text.
    await tester.tapAt(endpoints[0].point + const Offset(1.0, 13.0));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

    // Select all should select all the text.
    await tester.tap(find.text('Select all'));
    await tester.pump();
    expect(controller.selection.baseOffset, 0);
    expect(controller.selection.extentOffset, testValue.length);

    // Copy should reset the selection.
    await tester.tap(find.text('Copy'));
    await skipPastScrollingAnimation(tester);
    expect(controller.selection.isCollapsed, true);
  });

  testWidgets('Selectable height with maxLine', (WidgetTester tester) async {
    await tester.pumpWidget(selectableTextBuilder());

    RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText));

    final RenderBox textBox = findTextBox();
    final Size emptyInputSize = textBox.size;

    await tester.pumpWidget(selectableTextBuilder(text: 'No wrapping here.'));
    expect(findTextBox(), equals(textBox));
    expect(textBox.size.height, emptyInputSize.height);

    // Even when entering multiline text, SelectableText doesn't grow. It's a single
    // line input.
    await tester.pumpWidget(selectableTextBuilder(text: kThreeLines));
    expect(findTextBox(), equals(textBox));
    expect(textBox.size.height, emptyInputSize.height);

    // maxLines: 3 makes the SelectableText 3 lines tall
    await tester.pumpWidget(selectableTextBuilder(maxLines: 3));
    expect(findTextBox(), equals(textBox));
    expect(textBox.size.height, greaterThan(emptyInputSize.height));

    final Size threeLineInputSize = textBox.size;

    // Filling with 3 lines of text stays the same size
    await tester.pumpWidget(selectableTextBuilder(text: kThreeLines, maxLines: 3));
    expect(findTextBox(), equals(textBox));
    expect(textBox.size.height, threeLineInputSize.height);

    // An extra line won't increase the size because we max at 3.
    await tester.pumpWidget(selectableTextBuilder(text: kMoreThanFourLines, maxLines: 3));
    expect(findTextBox(), equals(textBox));
    expect(textBox.size.height, threeLineInputSize.height);

    // But now it will... but it will max at four
    await tester.pumpWidget(selectableTextBuilder(text: kMoreThanFourLines, maxLines: 4));
    expect(findTextBox(), equals(textBox));
    expect(textBox.size.height, greaterThan(threeLineInputSize.height));

    final Size fourLineInputSize = textBox.size;

    // Now it won't max out until the end
    await tester.pumpWidget(selectableTextBuilder(maxLines: null));
    expect(findTextBox(), equals(textBox));
    expect(textBox.size, equals(emptyInputSize));
    await tester.pumpWidget(selectableTextBuilder(text: kThreeLines, maxLines: null));
    expect(textBox.size.height, equals(threeLineInputSize.height));
    await tester.pumpWidget(selectableTextBuilder(text: kMoreThanFourLines, maxLines: null));
    expect(textBox.size.height, greaterThan(fourLineInputSize.height));
  });

  testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
    const String testValue = kThreeLines;
    await tester.pumpWidget(
      overlay(
        child: const SelectableText(
          testValue,
          dragStartBehavior: DragStartBehavior.down,
          style: TextStyle(color: Colors.black, fontSize: 34.0),
          maxLines: 3,
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;

    // Check that the text spans multiple lines.
    final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
    final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
    final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
    final Offset middleStringPos = textOffsetToPosition(tester, testValue.indexOf('irst'));

    expect(firstPos.dx, 24.5);
    expect(secondPos.dx, 24.5);
    expect(thirdPos.dx, 24.5);
    expect(middleStringPos.dx, 58.5);
    expect(firstPos.dx, secondPos.dx);
    expect(firstPos.dx, thirdPos.dx);
    expect(firstPos.dy, lessThan(secondPos.dy));
    expect(secondPos.dy, lessThan(thirdPos.dy));

    // Long press the 'n' in 'until' to select the word.
    final Offset untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1);
    TestGesture gesture = await tester.startGesture(untilPos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero

    expect(controller.selection.baseOffset, 39);
    expect(controller.selection.extentOffset, 44);

    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    // Drag the right handle to the third line, just after 'Third'.
    Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    // The distance below the y value returned by textOffsetToPosition required
    // to register a full vertical line drag.
    const Offset downLineOffset = Offset(0.0, 3.0);
    Offset newHandlePos =
        textOffsetToPosition(tester, testValue.indexOf('Third') + 5) + downLineOffset;
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 39,
        extentOffset: 50,
      ),
    );

    // Drag the left handle to the first line, just after 'First'.
    handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
    newHandlePos = textOffsetToPosition(tester, testValue.indexOf('First') + 5);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pump();

    expect(controller.selection.baseOffset, 5);
    expect(controller.selection.extentOffset, 50);
    await tester.tap(find.text('Copy'));
    await tester.pump();
    expect(controller.selection.isCollapsed, true);
  });

  testWidgets('Can scroll multiline input', (WidgetTester tester) async {
    await tester.pumpWidget(
      overlay(
        child: const SelectableText(
          kMoreThanFourLines,
          dragStartBehavior: DragStartBehavior.down,
          style: TextStyle(color: Colors.black, fontSize: 34.0),
          maxLines: 2,
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText));
    final TextEditingController controller = editableTextWidget.controller;
    RenderBox findInputBox() => tester.renderObject(find.byType(SelectableText));
    final RenderBox inputBox = findInputBox();

    // Check that the last line of text is not displayed.
    final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
    final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
    expect(firstPos.dx, 0.0);
    expect(fourthPos.dx, 0.0);
    expect(firstPos.dx, fourthPos.dx);
    expect(firstPos.dy, lessThan(fourthPos.dy));
    expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue);
    expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(fourthPos)), isFalse);

    TestGesture gesture = await tester.startGesture(firstPos, pointer: 7);
    await tester.pump();
    await gesture.moveBy(const Offset(0.0, -1000.0));
    await tester.pump(const Duration(seconds: 1));
    // Wait and drag again to trigger https://github.com/flutter/flutter/issues/6329
    // (No idea why this is necessary, but the bug wouldn't repro without it.)
    await gesture.moveBy(const Offset(0.0, -1000.0));
    await tester.pump(const Duration(seconds: 1));
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    // Now the first line is scrolled up, and the fourth line is visible.
    Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
    Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));

    expect(newFirstPos.dy, lessThan(firstPos.dy));
    expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse);
    expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue);

    // Now try scrolling by dragging the selection handle.
    // Long press the middle of the word "won't" in the fourth line.
    final Offset selectedWordPos = textOffsetToPosition(
      tester,
      kMoreThanFourLines.indexOf('Fourth line') + 14,
    );

    gesture = await tester.startGesture(selectedWordPos, pointer: 7);
    await tester.pump(const Duration(seconds: 1));
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(controller.selection.base.offset, 77);
    expect(controller.selection.extent.offset, 82);
    // Sanity check for the word selected is the intended one.
    expect(
      controller.text.substring(controller.selection.baseOffset, controller.selection.extentOffset),
      "won't",
    );

    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
    expect(endpoints.length, 2);

    // Drag the left handle to the first line, just after 'First'.
    final Offset handlePos = endpoints[0].point + const Offset(-1, 1);
    final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump(const Duration(seconds: 1));
    await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0));
    await tester.pump(const Duration(seconds: 1));
    await gesture.up();
    await tester.pump(const Duration(seconds: 1));

    // The text should have scrolled up with the handle to keep the active
    // cursor visible, back to its original position.
    newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
    newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
    expect(newFirstPos.dy, firstPos.dy);
    expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue);
    expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
  });

  testWidgets('minLines cannot be greater than maxLines', (WidgetTester tester) async {
    expect(
      () async {
        await tester.pumpWidget(
          overlay(
            child: SizedBox(
              width: 300.0,
              child: SelectableText(
                'abcd',
                minLines: 4,
                maxLines: 3,
              ),
            ),
          ),
        );
      },
      throwsA(isA<AssertionError>().having(
        (AssertionError error) => error.toString(),
        '.toString()',
        contains("minLines can't be greater than maxLines"),
      )),
    );
  });

  testWidgets('Selectable height with minLine', (WidgetTester tester) async {
    await tester.pumpWidget(selectableTextBuilder());

    RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText));

    final RenderBox textBox = findTextBox();
    final Size emptyInputSize = textBox.size;

    // Even if the text is a one liner, minimum height of SelectableText will determined by minLines
    await tester.pumpWidget(selectableTextBuilder(text: 'No wrapping here.', minLines: 2, maxLines: 3));
    expect(findTextBox(), equals(textBox));
    expect(textBox.size.height, emptyInputSize.height * 2);
  });

  testWidgets('Can align to center', (WidgetTester tester) async {
    await tester.pumpWidget(
      overlay(
        child: const SizedBox(
          width: 300.0,
          child: SelectableText(
            'abcd',
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );

    final RenderEditable editable = findRenderEditable(tester);

    final Offset topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
    );

    expect(topLeft.dx, equals(399.0));
  });

  testWidgets('Can align to center within center', (WidgetTester tester) async {
    await tester.pumpWidget(
      overlay(
        child: const SizedBox(
          width: 300.0,
          child: Center(
            child: SelectableText(
              'abcd',
              textAlign: TextAlign.center,
            ),
          ),
        ),
      ),
    );

    final RenderEditable editable = findRenderEditable(tester);

    final Offset topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
    );

    expect(topLeft.dx, equals(399.0));
  });

  testWidgets('Selectable text is skipped during focus traversal', (WidgetTester tester) async {
    final FocusNode firstFieldFocus = FocusNode();
    final FocusNode lastFieldFocus = FocusNode();

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Center(
            child: Column(
              children: <Widget>[
                TextField(
                  focusNode: firstFieldFocus,
                  autofocus: true,
                ),
                const SelectableText('some text'),
                TextField(
                  focusNode: lastFieldFocus,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    await tester.pump();

    expect(firstFieldFocus.hasFocus, isTrue);
    expect(lastFieldFocus.hasFocus, isFalse);

    firstFieldFocus.nextFocus();
    await tester.pump();

    // expecting focus to skip straight to the second field
    expect(firstFieldFocus.hasFocus, isFalse);
    expect(lastFieldFocus.hasFocus, isTrue);
  });

  testWidgets('Selectable text identifies as text field in semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText('some text'),
          ),
        ),
      ),
    );

    expect(
      semantics,
      includesNodeWith(
        flags: <SemanticsFlag>[
          SemanticsFlag.isTextField,
          SemanticsFlag.isReadOnly,
          SemanticsFlag.isMultiline,
        ],
      ),
    );

    semantics.dispose();
  });

  testWidgets('Selectable text rich text with spell out in semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText.rich(TextSpan(text: 'some text', spellOut: true)),
          ),
        ),
      ),
    );

    expect(
      semantics,
      includesNodeWith(
        attributedValue: AttributedString(
          'some text',
          attributes: <StringAttribute>[
            SpellOutStringAttribute(range: const TextRange(start: 0, end:9)),
          ],
        ),
        flags: <SemanticsFlag>[
          SemanticsFlag.isTextField,
          SemanticsFlag.isReadOnly,
          SemanticsFlag.isMultiline,
        ],
      ),
    );

    semantics.dispose();
  });

  testWidgets('Selectable text rich text with locale in semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText.rich(TextSpan(text: 'some text', locale: Locale('es', 'MX'))),
          ),
        ),
      ),
    );

    expect(
      semantics,
      includesNodeWith(
        attributedValue: AttributedString(
          'some text',
          attributes: <StringAttribute>[
            LocaleStringAttribute(range: const TextRange(start: 0, end:9), locale: const Locale('es', 'MX')),
          ],
        ),
        flags: <SemanticsFlag>[
          SemanticsFlag.isTextField,
          SemanticsFlag.isReadOnly,
          SemanticsFlag.isMultiline,
        ],
      ),
    );

    semantics.dispose();
  });

  testWidgets('Selectable rich text with gesture recognizer has correct semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await tester.pumpWidget(
      overlay(
        child: SelectableText.rich(
          TextSpan(
            children: <TextSpan>[
              const TextSpan(text: 'text'),
              TextSpan(
                text: 'link',
                recognizer: TapGestureRecognizer()
                  ..onTap = () { },
              ),
            ],
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          actions: <SemanticsAction>[SemanticsAction.longPress],
          textDirection: TextDirection.ltr,
          children: <TestSemantics>[
            TestSemantics(
              id: 2,
              children: <TestSemantics>[
                TestSemantics(
                  id: 3,
                  label: 'text',
                  textDirection: TextDirection.ltr,
                ),
                TestSemantics(
                  id: 4,
                  flags: <SemanticsFlag>[SemanticsFlag.isLink],
                  actions: <SemanticsAction>[SemanticsAction.tap],
                  label: 'link',
                  textDirection: TextDirection.ltr,
                ),
              ],
            ),
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

  group('Keyboard Tests', () {
    late TextEditingController controller;

    Future<void> setupWidget(WidgetTester tester, String text) async {
      final FocusNode focusNode = FocusNode();
      await tester.pumpWidget(
        MaterialApp(
          home: Material(
            child: RawKeyboardListener(
              focusNode: focusNode,
              child: SelectableText(
                text,
                maxLines: 3,
              ),
            ),
          ),
        ),
      );
      await tester.tap(find.byType(SelectableText));
      await tester.pumpAndSettle();
      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      controller = editableTextWidget.controller;
    }

    testWidgets('Shift test 1', (WidgetTester tester) async {
      await setupWidget(tester, 'a big house');

      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
      expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
    }, variant: KeySimulatorTransitModeVariant.all());

    testWidgets('Shift test 2', (WidgetTester tester) async {
      await setupWidget(tester, 'abcdefghi');

      controller.selection = const TextSelection.collapsed(offset: 3);
      await tester.pump();

      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
      await tester.pumpAndSettle();
      expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
    }, variant: KeySimulatorTransitModeVariant.all());

    testWidgets('Control Shift test', (WidgetTester tester) async {
      await setupWidget(tester, 'their big house');

      await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);

      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
    }, variant: KeySimulatorTransitModeVariant.all());

    testWidgets('Down and up test', (WidgetTester tester) async {
      await setupWidget(tester, 'a big house');

      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowUp);
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, -11);

      await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowUp);
      await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowDown);
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
    }, variant: KeySimulatorTransitModeVariant.all());

    testWidgets('Down and up test 2', (WidgetTester tester) async {
      await setupWidget(tester, 'a big house\njumped over a mouse\nOne more line yay');

      controller.selection = const TextSelection.collapsed(offset: 0);
      await tester.pump();

      for (int i = 0; i < 5; i += 1) {
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
        await tester.pumpAndSettle();
      }
      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
      await tester.pumpAndSettle();
      await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);

      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
      await tester.pumpAndSettle();
      await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 32);

      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
      await tester.pumpAndSettle();
      await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);

      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
      await tester.pumpAndSettle();
      await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);

      await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
      await tester.pumpAndSettle();
      await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
    }, variant: KeySimulatorTransitModeVariant.all());
  });

  testWidgets('Copy test', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();

    String clipboardContent = '';
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
      if (methodCall.method == 'Clipboard.setData') {
        clipboardContent = (methodCall.arguments as Map<String, dynamic>)['text'] as String;
      } else if (methodCall.method == 'Clipboard.getData') {
        return <String, dynamic>{'text': clipboardContent};
      }
      return null;
    });
    const String testValue = 'a big house\njumped over a mouse';
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: RawKeyboardListener(
            focusNode: focusNode,
            child: const SelectableText(
              testValue,
              maxLines: 3,
            ),
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;
    focusNode.requestFocus();
    await tester.pump();

    await tester.tap(find.byType(SelectableText));
    await tester.pumpAndSettle();

    controller.selection = const TextSelection.collapsed(offset: 0);
    await tester.pump();

    // Select the first 5 characters
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    for (int i = 0; i < 5; i += 1) {
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
      await tester.pumpAndSettle();
    }
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);

    // Copy them
    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
    await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
    await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
    await tester.pumpAndSettle();

    expect(clipboardContent, 'a big');

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
    await tester.pumpAndSettle();
  }, variant: KeySimulatorTransitModeVariant.all());

  testWidgets('Select all test', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    const String testValue = 'a big house\njumped over a mouse';
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: RawKeyboardListener(
            focusNode: focusNode,
            child: const SelectableText(
              testValue,
              maxLines: 3,
            ),
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;
    focusNode.requestFocus();
    await tester.pump();

    await tester.tap(find.byType(SelectableText));
    await tester.pumpAndSettle();

    // Select All
    await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
    await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
    await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
    await tester.pumpAndSettle();

    expect(controller.selection.baseOffset, 0);
    expect(controller.selection.extentOffset, 31);
  }, variant: KeySimulatorTransitModeVariant.all());

  testWidgets('keyboard selection should call onSelectionChanged', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    TextSelection? newSelection;
    const String testValue = 'a big house\njumped over a mouse';
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: RawKeyboardListener(
            focusNode: focusNode,
            child: SelectableText(
              testValue,
              maxLines: 3,
              onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
                expect(newSelection, isNull);
                newSelection = selection;
              },
            ),
          ),
        ),
      ),
    );
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;
    focusNode.requestFocus();
    await tester.pump();

    await tester.tap(find.byType(SelectableText));
    await tester.pumpAndSettle();
    expect(newSelection!.baseOffset, 31);
    expect(newSelection!.extentOffset, 31);
    newSelection = null;

    controller.selection = const TextSelection.collapsed(offset: 0);
    await tester.pump();

    // Select the first 5 characters
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    for (int i = 0; i < 5; i += 1) {
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
      await tester.pumpAndSettle();
      expect(newSelection!.baseOffset, 0);
      expect(newSelection!.extentOffset, i + 1);
      newSelection = null;
    }
  }, variant: KeySimulatorTransitModeVariant.all());

  testWidgets('Changing positions of selectable text', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    final List<RawKeyEvent> events = <RawKeyEvent>[];

    final Key key1 = UniqueKey();
    final Key key2 = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home:
        Material(
          child: RawKeyboardListener(
            focusNode: focusNode,
            onKey: events.add,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                SelectableText(
                  'a big house',
                  key: key1,
                  maxLines: 3,
                ),
                SelectableText(
                  'another big house',
                  key: key2,
                  maxLines: 3,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    TextEditingController c1 = editableTextWidget.controller;

    await tester.tap(find.byType(EditableText).first);
    await tester.pumpAndSettle();

    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    for (int i = 0; i < 5; i += 1) {
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
      await tester.pumpAndSettle();
    }
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    await tester.pumpAndSettle();

    expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);

    await tester.pumpWidget(
      MaterialApp(
        home:
        Material(
          child: RawKeyboardListener(
            focusNode: focusNode,
            onKey: events.add,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                SelectableText(
                  'another big house',
                  key: key2,
                  maxLines: 3,
                ),
                SelectableText(
                  'a big house',
                  key: key1,
                  maxLines: 3,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    for (int i = 0; i < 5; i += 1) {
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
    }
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    await tester.pumpAndSettle();

    editableTextWidget = tester.widget(find.byType(EditableText).last);
    c1 = editableTextWidget.controller;

    expect(c1.selection.extentOffset - c1.selection.baseOffset, -10);
  }, variant: KeySimulatorTransitModeVariant.all());

  testWidgets('Changing focus test', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    final List<RawKeyEvent> events = <RawKeyEvent>[];

    final Key key1 = UniqueKey();
    final Key key2 = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home:
        Material(
          child: RawKeyboardListener(
            focusNode: focusNode,
            onKey: events.add,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                SelectableText(
                  'a big house',
                  key: key1,
                  maxLines: 3,
                ),
                SelectableText(
                  'another big house',
                  key: key2,
                  maxLines: 3,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    final EditableText editableTextWidget1 = tester.widget(find.byType(EditableText).first);
    final TextEditingController c1 = editableTextWidget1.controller;

    final EditableText editableTextWidget2 = tester.widget(find.byType(EditableText).last);
    final TextEditingController c2 = editableTextWidget2.controller;

    await tester.tap(find.byType(SelectableText).first);
    await tester.pumpAndSettle();

    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    for (int i = 0; i < 5; i += 1) {
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
      await tester.pumpAndSettle();
    }
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    await tester.pumpAndSettle();

    expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
    expect(c2.selection.extentOffset - c2.selection.baseOffset, 0);

    await tester.tap(find.byType(SelectableText).last);
    await tester.pumpAndSettle();

    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    for (int i = 0; i < 5; i += 1) {
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
      await tester.pumpAndSettle();
    }
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    await tester.pumpAndSettle();

    expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
    expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
  }, variant: KeySimulatorTransitModeVariant.all());

  testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const SelectableText(
            'x',
          ),
        ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;

    expect(controller.selection.baseOffset, -1);

    // Tap the selection handle to bring up the "paste / select all" menu.
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is

    // Confirm that the selection was updated.
    expect(controller.selection.baseOffset, 0);
  });

  testWidgets('SelectableText baseline alignment no-strut', (WidgetTester tester) async {
    final Key keyA = UniqueKey();
    final Key keyB = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.baseline,
          textBaseline: TextBaseline.alphabetic,
          children: <Widget>[
            Expanded(
              child: SelectableText(
                'A',
                key: keyA,
                style: const TextStyle(fontSize: 10.0),
                strutStyle: StrutStyle.disabled,
              ),
            ),
            const Text(
              'abc',
              style: TextStyle(fontSize: 20.0),
            ),
            Expanded(
              child: SelectableText(
                'B',
                key: keyB,
                style: const TextStyle(fontSize: 30.0),
                strutStyle: StrutStyle.disabled,
              ),
            ),
          ],
        ),
      ),
    );

    // The Ahem font extends 0.2 * fontSize below the baseline.
    // So the three row elements line up like this:
    //
    //  A  abc  B
    //  ---------   baseline
    //  2  4    6   space below the baseline = 0.2 * fontSize
    //  ---------   rowBottomY

    final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy;
    expect(tester.getBottomLeft(find.byKey(keyA)).dy, moreOrLessEquals(rowBottomY - 4.0, epsilon: 1e-3));
    expect(tester.getBottomLeft(find.text('abc')).dy, moreOrLessEquals(rowBottomY - 2.0, epsilon: 1e-3));
    expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
  });

  testWidgets('SelectableText baseline alignment', (WidgetTester tester) async {
    final Key keyA = UniqueKey();
    final Key keyB = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.baseline,
          textBaseline: TextBaseline.alphabetic,
          children: <Widget>[
            Expanded(
              child: SelectableText(
                'A',
                key: keyA,
                style: const TextStyle(fontSize: 10.0),
              ),
            ),
            const Text(
              'abc',
              style: TextStyle(fontSize: 20.0),
            ),
            Expanded(
              child: SelectableText(
                'B',
                key: keyB,
                style: const TextStyle(fontSize: 30.0),
              ),
            ),
          ],
        ),
      ),
    );

    // The Ahem font extends 0.2 * fontSize below the baseline.
    // So the three row elements line up like this:
    //
    //  A  abc  B
    //  ---------   baseline
    //  2  4    6   space below the baseline = 0.2 * fontSize
    //  ---------   rowBottomY

    final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy;
    expect(tester.getBottomLeft(find.byKey(keyA)).dy, moreOrLessEquals(rowBottomY - 4.0, epsilon: 1e-3));
    expect(tester.getBottomLeft(find.text('abc')).dy, moreOrLessEquals(rowBottomY - 2.0, epsilon: 1e-3));
    expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
  });

  testWidgets('SelectableText semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final Key key = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: SelectableText(
          'Guten Tag',
          key: key,
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          textDirection: TextDirection.ltr,
          value: 'Guten Tag',
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isMultiline,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    await tester.tap(find.byKey(key));
    await tester.pump();

    controller.selection = const TextSelection.collapsed(offset: 9);
    await tester.pump();

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          textDirection: TextDirection.ltr,
          value: 'Guten Tag',
          textSelection: const TextSelection.collapsed(offset: 9),
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
            SemanticsAction.moveCursorBackwardByCharacter,
            SemanticsAction.moveCursorBackwardByWord,
            SemanticsAction.setSelection,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    controller.selection = const TextSelection.collapsed(offset: 4);
    await tester.pump();

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          textDirection: TextDirection.ltr,
          textSelection: const TextSelection.collapsed(offset: 4),
          value: 'Guten Tag',
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
            SemanticsAction.moveCursorBackwardByCharacter,
            SemanticsAction.moveCursorForwardByCharacter,
            SemanticsAction.moveCursorBackwardByWord,
            SemanticsAction.moveCursorForwardByWord,
            SemanticsAction.setSelection,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    controller.selection = const TextSelection.collapsed(offset: 0);
    await tester.pump();

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          textDirection: TextDirection.ltr,
          textSelection: const TextSelection.collapsed(offset: 0),
          value: 'Guten Tag',
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
            SemanticsAction.moveCursorForwardByCharacter,
            SemanticsAction.moveCursorForwardByWord,
            SemanticsAction.setSelection,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

  testWidgets('SelectableText semantics, with semanticsLabel', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final Key key = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: SelectableText(
          'Guten Tag',
          semanticsLabel: 'German greeting for good day',
          key: key,
        ),
      ),
    );

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics(
          id: 1,
          actions: <SemanticsAction>[SemanticsAction.longPress],
          label: 'German greeting for good day',
          textDirection: TextDirection.ltr,
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));
  });

  testWidgets('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final Key key = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: SelectableText(
          'Guten Tag',
          key: key,
          enableInteractiveSelection: false,
        ),
      ),
    );

    await tester.tap(find.byKey(key));
    await tester.pump();

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          value: 'Guten Tag',
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
            // Absent the following because enableInteractiveSelection: false
            // SemanticsAction.moveCursorBackwardByCharacter,
            // SemanticsAction.moveCursorBackwardByWord,
            // SemanticsAction.setSelection,
            // SemanticsAction.paste,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
            // SelectableText act like a text widget when enableInteractiveSelection
            // is false. It will not respond to any pointer event.
            // SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

  testWidgets('SelectableText semantics for selections', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final Key key = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: SelectableText(
          'Hello',
          key: key,
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          value: 'Hello',
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    // Focus the selectable text
    await tester.tap(find.byKey(key));
    await tester.pump();

    controller.selection = const TextSelection.collapsed(offset: 5);
    await tester.pump();

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          value: 'Hello',
          textSelection: const TextSelection.collapsed(offset: 5),
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
            SemanticsAction.moveCursorBackwardByCharacter,
            SemanticsAction.moveCursorBackwardByWord,
            SemanticsAction.setSelection,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    controller.selection = const TextSelection(baseOffset: 5, extentOffset: 3);
    await tester.pump();

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          value: 'Hello',
          textSelection: const TextSelection(baseOffset: 5, extentOffset: 3),
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
            SemanticsAction.moveCursorBackwardByCharacter,
            SemanticsAction.moveCursorForwardByCharacter,
            SemanticsAction.moveCursorBackwardByWord,
            SemanticsAction.moveCursorForwardByWord,
            SemanticsAction.setSelection,
            SemanticsAction.copy,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

  testWidgets('semantic nodes of offscreen recognizers are marked hidden', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/100395.
    final SemanticsTester semantics = SemanticsTester(tester);
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem', fontSize: 200);
    const String onScreenText = 'onscreen\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
    const String offScreenText = 'off screen';
    final ScrollController controller = ScrollController();
    await tester.pumpWidget(
      MaterialApp(
        home: SingleChildScrollView(
          controller: controller,
          child: SelectableText.rich(
            TextSpan(
              children: <TextSpan>[
                const TextSpan(text: onScreenText),
                TextSpan(
                  text: offScreenText,
                  recognizer: TapGestureRecognizer()..onTap = () { },
                ),
              ],
              style: textStyle,
            ),
            textDirection: TextDirection.ltr,
          ),
        )
      ),
    );

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics(
          textDirection: TextDirection.ltr,
          children: <TestSemantics>[
            TestSemantics(
              children: <TestSemantics>[
                TestSemantics(
                  flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                  children: <TestSemantics>[
                    TestSemantics(
                      flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
                      actions: <SemanticsAction>[SemanticsAction.scrollUp],
                      children: <TestSemantics>[
                        TestSemantics(
                          actions: <SemanticsAction>[SemanticsAction.longPress],
                          children: <TestSemantics>[
                            TestSemantics(
                              children: <TestSemantics>[
                                TestSemantics(
                                  label: 'onscreen\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n',
                                  textDirection: TextDirection.ltr,
                                ),
                                TestSemantics(
                                  flags: <SemanticsFlag>[
                                    SemanticsFlag.isHidden,
                                    SemanticsFlag.isLink,
                                  ],
                                  actions: <SemanticsAction>[SemanticsAction.tap],
                                  label: 'off screen',
                                  textDirection: TextDirection.ltr,
                                ),
                              ],
                            ),
                          ],
                        ),
                      ],
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ],
    );
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
        ignoreRect: true,
      ),
    );

    // Test show on screen.
    expect(controller.offset, 0.0);
    tester.binding.pipelineOwner.semanticsOwner!.performAction(8, SemanticsAction.showOnScreen);
    await tester.pumpAndSettle();
    expect(controller.offset != 0.0, isTrue);

    semantics.dispose();
  });

  testWidgets('SelectableText change selection with semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
    final Key key = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: SelectableText(
          'Hello',
          key: key,
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;

    // Focus the selectable text
    await tester.tap(find.byKey(key));
    await tester.pump();

    controller.selection = const TextSelection(baseOffset: 5, extentOffset: 5);
    await tester.pump();

    const int inputFieldId = 1;

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: inputFieldId,
          value: 'Hello',
          textSelection: const TextSelection.collapsed(offset: 5),
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
            SemanticsAction.moveCursorBackwardByCharacter,
            SemanticsAction.moveCursorBackwardByWord,
            SemanticsAction.setSelection,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    // move cursor back once
    semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
      'base': 4,
      'extent': 4,
    });
    await tester.pump();
    expect(controller.selection, const TextSelection.collapsed(offset: 4));

    // move cursor to front
    semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
      'base': 0,
      'extent': 0,
    });
    await tester.pump();
    expect(controller.selection, const TextSelection.collapsed(offset: 0));

    // select all
    semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <dynamic, dynamic>{
      'base': 0,
      'extent': 5,
    });
    await tester.pump();
    expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: inputFieldId,
          value: 'Hello',
          textSelection: const TextSelection(baseOffset: 0, extentOffset: 5),
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.longPress,
            SemanticsAction.moveCursorBackwardByCharacter,
            SemanticsAction.moveCursorBackwardByWord,
            SemanticsAction.setSelection,
            SemanticsAction.copy,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isReadOnly,
            SemanticsFlag.isTextField,
            SemanticsFlag.isMultiline,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

  testWidgets('Can activate SelectableText with explicit controller via semantics', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/17801

    const String testValue = 'Hello';

    final SemanticsTester semantics = SemanticsTester(tester);
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
    final Key key = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: SelectableText(
          testValue,
          key: key,
        ),
      ),
    );

    const int inputFieldId = 1;

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            id: inputFieldId,
            flags: <SemanticsFlag>[
              SemanticsFlag.isReadOnly,
              SemanticsFlag.isTextField,
              SemanticsFlag.isMultiline,
            ],
            actions: <SemanticsAction>[SemanticsAction.longPress],
            value: testValue,
            textDirection: TextDirection.ltr,
          ),
        ],
      ),
      ignoreRect: true, ignoreTransform: true,
    ));

    semanticsOwner.performAction(inputFieldId, SemanticsAction.longPress);
    await tester.pump();

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            id: inputFieldId,
            flags: <SemanticsFlag>[
              SemanticsFlag.isReadOnly,
              SemanticsFlag.isTextField,
              SemanticsFlag.isMultiline,
              SemanticsFlag.isFocused,
            ],
            actions: <SemanticsAction>[
              SemanticsAction.longPress,
              SemanticsAction.moveCursorBackwardByCharacter,
              SemanticsAction.moveCursorBackwardByWord,
              SemanticsAction.setSelection,
            ],
            value: testValue,
            textDirection: TextDirection.ltr,
            textSelection: const TextSelection(
              baseOffset: testValue.length,
              extentOffset: testValue.length,
            ),
          ),
        ],
      ),
      ignoreRect: true, ignoreTransform: true,
    ));

    semantics.dispose();
  });

  testWidgets('SelectableText throws when not descended from a MediaQuery widget', (WidgetTester tester) async {
    const Widget selectableText = SelectableText('something');
    await tester.pumpWidget(selectableText);
    final dynamic exception = tester.takeException();
    expect(exception, isFlutterError);
    expect(exception.toString(), startsWith('No MediaQuery widget ancestor found.\nSelectableText widgets require a MediaQuery widget ancestor.'));
  });

  testWidgets('onTap is called upon tap', (WidgetTester tester) async {
    int tapCount = 0;
    await tester.pumpWidget(
      overlay(
        child: SelectableText(
          'something',
          onTap: () {
            tapCount += 1;
          },
        ),
      ),
    );

    expect(tapCount, 0);
    await tester.tap(find.byType(SelectableText));
    // Wait a bit so they're all single taps and not double taps.
    await tester.pump(const Duration(milliseconds: 300));
    await tester.tap(find.byType(SelectableText));
    await tester.pump(const Duration(milliseconds: 300));
    await tester.tap(find.byType(SelectableText));
    await tester.pump(const Duration(milliseconds: 300));
    expect(tapCount, 3);
  });

  testWidgets('SelectableText style is merged with default text style', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/23994
    final TextStyle defaultStyle = TextStyle(
      color: Colors.blue[500],
    );
    Widget buildFrame(TextStyle style) {
      return MaterialApp(
        home: Material(
          child: DefaultTextStyle (
            style: defaultStyle,
            child: Center(
              child: SelectableText(
                'something',
                style: style,
              ),
            ),
          ),
        ),
      );
    }

    // Empty TextStyle is overridden by theme
    await tester.pumpWidget(buildFrame(const TextStyle()));
    EditableText editableText = tester.widget(find.byType(EditableText));
    expect(editableText.style.color, defaultStyle.color);
    expect(editableText.style.background, defaultStyle.background);
    expect(editableText.style.shadows, defaultStyle.shadows);
    expect(editableText.style.decoration, defaultStyle.decoration);
    expect(editableText.style.locale, defaultStyle.locale);
    expect(editableText.style.wordSpacing, defaultStyle.wordSpacing);

    // Properties set on TextStyle override theme
    const Color setColor = Colors.red;
    await tester.pumpWidget(buildFrame(const TextStyle(color: setColor)));
    editableText = tester.widget(find.byType(EditableText));
    expect(editableText.style.color, setColor);

    // inherit: false causes nothing to be merged in from theme
    await tester.pumpWidget(buildFrame(const TextStyle(
      fontSize: 24.0,
      textBaseline: TextBaseline.alphabetic,
      inherit: false,
    )));
    editableText = tester.widget(find.byType(EditableText));
    expect(editableText.style.color, isNull);
  });

  testWidgets('style enforces required fields', (WidgetTester tester) async {
    Widget buildFrame(TextStyle style) {
      return MaterialApp(
        home: Material(
          child: SelectableText(
            'something',
            style: style,
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(const TextStyle(
      inherit: false,
      fontSize: 12.0,
      textBaseline: TextBaseline.alphabetic,
    )));
    expect(tester.takeException(), isNull);

    // With inherit not set to false, will pickup required fields from theme
    await tester.pumpWidget(buildFrame(const TextStyle(
      fontSize: 12.0,
    )));
    expect(tester.takeException(), isNull);

    await tester.pumpWidget(buildFrame(const TextStyle(
      inherit: false,
      fontSize: 12.0,
    )));
    expect(tester.takeException(), isNotNull);
  });

  testWidgets(
    'tap moves cursor to the edge of the word it tapped',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;
      // We moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );

      // But don't trigger the toolbar.
      expect(find.byType(CupertinoButton), findsNothing);
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'tap moves cursor to the position tapped (Android)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // We moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 4, affinity: TextAffinity.upstream),
      );

      // But don't trigger the toolbar.
      expect(find.byType(TextButton), findsNothing);
    },
  );

  testWidgets(
    'two slow taps do not trigger a word selection',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));
      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // Plain collapsed selection.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );

      // No toolbar.
      expect(find.byType(CupertinoButton), findsNothing);
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'double tap selects word and first tap of double tap moves cursor',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      // This tap just puts the cursor somewhere different than where the double
      // tap will occur to test that the double tap moves the existing cursor first.
      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump();

      // Second tap selects the word around the cursor.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      // Selected text shows 1 toolbar buttons.
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'double tap selects word and first tap of double tap moves cursor and shows toolbar (Android)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      // This tap just puts the cursor somewhere different than where the double
      // tap will occur to test that the double tap moves the existing cursor first.
      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump();

      // Second tap selects the word around the cursor.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      // Selected text shows 2 toolbar buttons: copy, select all
      expect(find.byType(TextButton), findsNWidgets(2));
    },
  );

  testWidgets(
    'double tap on top of cursor also selects word (Android)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      // Tap to put the cursor after the "w".
      const int index = 3;
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pump(const Duration(milliseconds: 500));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      expect(
        controller.selection,
        const TextSelection.collapsed(offset: index),
      );

      // Double tap on the same location.
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pump(const Duration(milliseconds: 50));

      // First tap doesn't change the selection
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: index),
      );

      // Second tap selects the word around the cursor.
      await tester.tapAt(textOffsetToPosition(tester, index));
      await tester.pump();
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );

      // Selected text shows 2 toolbar buttons: copy, select all
      expect(find.byType(TextButton), findsNWidgets(2));
    },
  );

  testWidgets(
    'double tap hold selects word',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      final TestGesture gesture =
      await tester.startGesture(selectableTextStart + const Offset(150.0, 5.0));
      // Hold the press.
      await tester.pump(const Duration(milliseconds: 500));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      // Selected text shows 1 toolbar buttons.
      expect(find.byType(CupertinoButton), findsNWidgets(1));

      await gesture.up();
      await tester.pump();

      // Still selected.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      // The toolbar is still showing.
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'double tap selects word with semantics label',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText.rich(
                TextSpan(text: 'Atwater Peel Sherbrooke Bonaventure', semanticsLabel: ''),
              ),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(220.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      await tester.tapAt(selectableTextStart + const Offset(220.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      expect(
        controller.selection,
        const TextSelection(baseOffset: 13, extentOffset: 23),
      );
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'tap after a double tap select is not affected (iOS)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.tapAt(selectableTextStart + const Offset(100.0, 5.0));
      await tester.pump();

      // Plain collapsed selection at the edge of first word. In iOS 12, the
      // first tap after a double tap ends up putting the cursor at where
      // you tapped instead of the edge like every other single tap. This is
      // likely a bug in iOS 12 and not present in other versions.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7),
      );

      // No toolbar.
      expect(find.byType(CupertinoButton), findsNothing);
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'long press selects word and shows toolbar (iOS)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // The longpressed word is selected.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 0,
          extentOffset: 7,
        ),
      );

      // Toolbar shows one button.
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'long press selects word and shows toolbar (Android)',
    (WidgetTester tester) async {

      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );

      // Collapsed toolbar shows 2 buttons: copy, select all
      expect(find.byType(TextButton), findsNWidgets(2));
    },
  );

  testWidgets(
    'long press selects word and shows custom toolbar (Android)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure',
              selectionControls: cupertinoTextSelectionControls,
              ),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // The longpressed word is selected.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 0,
          extentOffset: 7,
        ),
      );

      // Toolbar shows one button.
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: TargetPlatformVariant.all(),
  );

  testWidgets(
    'long press selects word and shows custom toolbar (iOS)',
    (WidgetTester tester) async {

      await tester.pumpWidget(
        MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure',
              selectionControls: materialTextSelectionControls,
              ),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );

      // Collapsed toolbar shows 2 buttons: copy, select all
      expect(find.byType(TextButton), findsNWidgets(2));
    },
    variant: TargetPlatformVariant.all(),
  );

  testWidgets(
    'textSelectionControls is passed to EditableText',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Material(
            child: Scaffold(
              body: SelectableText('Atwater Peel Sherbrooke Bonaventure',
                selectionControls: materialTextSelectionControls,
              ),
            ),
          ),
        ),
      );

      final EditableText widget = tester.widget(find.byType(EditableText));
      expect(widget.selectionControls, equals(materialTextSelectionControls));
    },
  );

  testWidgets(
    'long press tap cannot initiate a double tap',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      // Hide the toolbar so it doesn't interfere with taps on the text.
      final EditableTextState editableTextState =
          tester.state<EditableTextState>(find.byType(EditableText));
      editableTextState.hideToolbar();
      await tester.pumpAndSettle();

      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump();

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // We ended up moving the cursor to the edge of the same word and dismissed
      // the toolbar.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );

      expect(find.byType(CupertinoButton), findsNothing);
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final TestGesture gesture =
          await tester.startGesture(textOffsetToPosition(tester, 18));
      await tester.pump(const Duration(milliseconds: 500));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // Long press selects the word at the long presses position.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 13, extentOffset: 23),
      );
      // Cursor move doesn't trigger a toolbar initially.
      expect(find.byType(TextButton), findsNothing);

      await gesture.moveBy(const Offset(100, 0));
      await tester.pump();

      // The selection is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 13, extentOffset: 35),
      );
      // Still no toolbar.
      expect(find.byType(TextButton), findsNothing);

      // The selection is moved on a backwards drag.
      await gesture.moveBy(const Offset(-200, 0));
      await tester.pump();

      // The selection is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 23, extentOffset: 8),
      );
      // Still no toolbar.
      expect(find.byType(TextButton), findsNothing);

      await gesture.moveBy(const Offset(-100, 0));
      await tester.pump();

      // The selection is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 23, extentOffset: 0),
      );
      // Still no toolbar.
      expect(find.byType(TextButton), findsNothing);

      await gesture.up();
      await tester.pumpAndSettle();

      // The selection isn't affected by the gesture lift.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 23, extentOffset: 0),
      );
      // The toolbar now shows up.
      expect(find.byType(TextButton), findsNWidgets(2));
    },
    variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
  );

  testWidgets(
    'long press drag extends the selection to the word under the drag and shows toolbar on lift (iOS)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final TestGesture gesture =
          await tester.startGesture(textOffsetToPosition(tester, 18));
      await tester.pump(const Duration(milliseconds: 500));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // The longpressed word is selected.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 13,
          extentOffset: 23,
        ),
      );
      // Word select doesn't trigger a toolbar initially.
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.moveBy(const Offset(100, 0));
      await tester.pump();

      // The selection is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 13,
          extentOffset: 35,
        ),
      );
      // Still no toolbar.
      expect(find.byType(CupertinoButton), findsNothing);

      // The selection is moved with a backwards drag.
      await gesture.moveBy(const Offset(-200, 0));
      await tester.pump();

      // The selection is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 23,
          extentOffset: 8,
        ),
      );
      // Still no toolbar.
      expect(find.byType(CupertinoButton), findsNothing);

      // The selection is moved with a backwards drag.
      await gesture.moveBy(const Offset(-100, 0));
      await tester.pump();

      // The selection is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 23,
          extentOffset: 0,
        ),
      );
      // Still no toolbar.
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.up();
      await tester.pump();

      // The selection isn't affected by the gesture lift.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 23,
          extentOffset: 0,
        ),
      );
      // The toolbar now shows up.
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets(
    'long press drag moves the cursor under the drag and shows toolbar on lift (macOS)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      final TestGesture gesture =
      await tester.startGesture(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // The longpressed word is selected.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 0,
          extentOffset: 7,
        ),
      );
      // Cursor move doesn't trigger a toolbar initially.
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();

      // The selection position is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 0,
          extentOffset: 8,
        ),
      );
      // Still no toolbar.
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();

      // The selection position is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 0,
          extentOffset: 12,
        ),
      );
      // Still no toolbar.
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.up();
      await tester.pump();

      // The selection isn't affected by the gesture lift.
      expect(
        controller.selection,
        const TextSelection(
          baseOffset: 0,
          extentOffset: 12,
        ),
      );
      // The toolbar now shows up.
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }),
  );

  testWidgets('long press drag can edge scroll', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText(
              'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
              maxLines: 1,
            ),
          ),
        ),
      ),
    );

    final RenderEditable renderEditable = findRenderEditable(tester);

    List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 66), // Last character's position.
    );

    expect(lastCharEndpoint.length, 1);
    // Just testing the test and making sure that the last character is off
    // the right side of the screen.
    expect(lastCharEndpoint[0].point.dx, 924.0);

    final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

    final TestGesture gesture =
    await tester.startGesture(selectableTextStart + const Offset(300, 5));
    await tester.pump(const Duration(milliseconds: 500));

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;

    expect(
      controller.selection,
      const TextSelection(baseOffset: 13, extentOffset: 23),
    );
    expect(find.byType(CupertinoButton), findsNothing);

    await gesture.moveBy(const Offset(600, 0));
    // To the edge of the screen basically.
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 13,
        extentOffset: 66,
      ),
    );
    // Keep moving out.
    await gesture.moveBy(const Offset(1, 0));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 13,
        extentOffset: 66,
      ),
    );
    await gesture.moveBy(const Offset(1, 0));
    await tester.pump();
    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 13,
        extentOffset: 66,
      ),
    );
    expect(find.byType(CupertinoButton), findsNothing);

    await gesture.up();
    await tester.pump();

    // The selection isn't affected by the gesture lift.
    expect(
      controller.selection,
      const TextSelection(
        baseOffset: 13,
        extentOffset: 66,
      ),
    );
    // The toolbar now shows up.
    expect(find.byType(CupertinoButton), findsNWidgets(1));

    lastCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 66), // Last character's position.
    );

    expect(lastCharEndpoint.length, 1);
    // The last character is now on screen near the right edge.
    expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1));

    final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
      const TextSelection.collapsed(offset: 0), // First character's position.
    );
    expect(firstCharEndpoint.length, 1);
    // The first character is now offscreen to the left.
    expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-125, epsilon: 1));
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
    skip: true, // https://github.com/flutter/flutter/issues/64059
  );

  testWidgets(
    'long tap still selects after a double tap select (iOS)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // First tap moved the cursor to the beginning of the second word.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0));
      await tester.pump();

      // Selected the "word" where the tap happened, which is the first space.
      // Because the "word" is a whitespace, the selection will shift to the
      // previous "word" that is not a whitespace.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );

      // Long press toolbar.
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
  );

  testWidgets(
    'long tap still selects after a double tap select (macOS)',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // First tap moved the cursor to the beginning of the second word.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.longPressAt(selectableTextStart + const Offset(100.0, 5.0));
      await tester.pump();

      // Selected the "word" where the tap happened, which is the first space.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 7, extentOffset: 8),
      );

      // Long press toolbar.
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }),
  );
//convert
  testWidgets(
    'double tap after a long tap is not affected',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );

      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      // Hide the toolbar so it doesn't interfere with taps on the text.
      final EditableTextState editableTextState =
          tester.state<EditableTextState>(find.byType(EditableText));
      editableTextState.hideToolbar();
      await tester.pumpAndSettle();

      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump();

      // Double tap selection.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets(
    'double tap chains work',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: Center(
              child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
            ),
          ),
        ),
      );
      final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
      final TextEditingController controller = editableTextWidget.controller;

      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(1));

      // Double tap selecting the same word somewhere else is fine.
      await tester.tapAt(selectableTextStart + const Offset(10.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(10.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(1));

      // Hide the toolbar so it doesn't interfere with taps on the text.
      final EditableTextState editableTextState =
          tester.state<EditableTextState>(find.byType(EditableText));
      editableTextState.hideToolbar();
      await tester.pumpAndSettle();

      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(1));
    },
    variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }),
  );

  testWidgets('force press does not select a word on (android)', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
        ),
      ),
    );

    final Offset offset = tester.getTopLeft(find.byType(SelectableText)) + const Offset(150.0, 5.0);

    final int pointerValue = tester.nextPointer;
    final TestGesture gesture = await tester.createGesture();
    await gesture.downWithCustomEvent(
      offset,
      PointerDownEvent(
        pointer: pointerValue,
        position: offset,
        pressure: 0.0,
        pressureMax: 6.0,
        pressureMin: 0.0,
      ),
    );
    await gesture.updateWithCustomEvent(PointerMoveEvent(pointer: pointerValue, position: offset + const Offset(150.0, 5.0), pressure: 0.5, pressureMin: 0));

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;

    // We don't want this gesture to select any word on Android.
    expect(controller.selection, const TextSelection.collapsed(offset: -1));

    await gesture.up();
    await tester.pump();
    expect(find.byType(TextButton), findsNothing);
  });

  testWidgets('force press selects word', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
        ),
      ),
    );

    final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

    final int pointerValue = tester.nextPointer;
    final Offset offset = selectableTextStart + const Offset(150.0, 5.0);
    final TestGesture gesture = await tester.createGesture();
    await gesture.downWithCustomEvent(
      offset,
      PointerDownEvent(
        pointer: pointerValue,
        position: offset,
        pressure: 0.0,
        pressureMax: 6.0,
        pressureMin: 0.0,
      ),
    );

    await gesture.updateWithCustomEvent(PointerMoveEvent(pointer: pointerValue, position: selectableTextStart + const Offset(150.0, 5.0), pressure: 0.5, pressureMin: 0));

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;

    // We expect the force press to select a word at the given location.
    expect(
      controller.selection,
      const TextSelection(baseOffset: 8, extentOffset: 12),
    );

    await gesture.up();
    await tester.pump();
    expect(find.byType(CupertinoButton), findsNWidgets(1));
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));

  testWidgets('tap on non-force-press-supported devices work', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
        ),
      ),
    );

    final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

    final int pointerValue = tester.nextPointer;
    final Offset offset = selectableTextStart + const Offset(150.0, 5.0);
    final TestGesture gesture = await tester.createGesture();
    await gesture.downWithCustomEvent(
      offset,
      PointerDownEvent(
        pointer: pointerValue,
        position: offset,
        // iPhone 6 and below report 0 across the board.
        pressure: 0,
        pressureMax: 0,
        pressureMin: 0,
      ),
    );

    await gesture.updateWithCustomEvent(PointerMoveEvent(pointer: pointerValue, position: selectableTextStart + const Offset(150.0, 5.0), pressure: 0.5, pressureMin: 0));
    await gesture.up();

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;

    // The event should fallback to a normal tap and move the cursor.
    // Single taps selects the edge of the word.
    expect(
      controller.selection,
      const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
    );

    await tester.pump();
    // Single taps shouldn't trigger the toolbar.
    expect(find.byType(CupertinoButton), findsNothing);

    // TODO(gspencergoog): Add in TargetPlatform.macOS in the line below when we
    // figure out what global state is leaking.
    // https://github.com/flutter/flutter/issues/43445
  }, variant: TargetPlatformVariant.only(TargetPlatform.iOS));

  testWidgets('default SelectableText debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

    const SelectableText('something').debugFillProperties(builder);

    final List<String> description = builder.properties
        .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
        .map((DiagnosticsNode node) => node.toString()).toList();

    expect(description, <String>['data: something']);
  });

  testWidgets('SelectableText implements debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

    // Not checking controller, inputFormatters, focusNode
    const SelectableText(
      'something',
      style: TextStyle(color: Color(0xff00ff00)),
      textAlign: TextAlign.end,
      textDirection: TextDirection.ltr,
      textScaleFactor: 1.0,
      autofocus: true,
      showCursor: true,
      minLines: 2,
      maxLines: 10,
      cursorWidth: 1.0,
      cursorHeight: 1.0,
      cursorRadius: Radius.zero,
      cursorColor: Color(0xff00ff00),
      scrollPhysics: ClampingScrollPhysics(),
      semanticsLabel: 'something else',
      enableInteractiveSelection: false,
    ).debugFillProperties(builder);

    final List<String> description = builder.properties
        .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
        .map((DiagnosticsNode node) => node.toString()).toList();

    expect(description, <String>[
      'data: something',
      'semanticsLabel: something else',
      'style: TextStyle(inherit: true, color: Color(0xff00ff00))',
      'autofocus: true',
      'showCursor: true',
      'minLines: 2',
      'maxLines: 10',
      'textAlign: end',
      'textDirection: ltr',
      'textScaleFactor: 1.0',
      'cursorWidth: 1.0',
      'cursorHeight: 1.0',
      'cursorRadius: Radius.circular(0.0)',
      'cursorColor: Color(0xff00ff00)',
      'selection disabled',
      'scrollPhysics: ClampingScrollPhysics',
    ]);
  });

  testWidgets(
    'strut basic single line',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: const Material(
            child: Center(
              child: SelectableText('something'),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(SelectableText)),
        // This is the height of the decoration (24) plus the metrics from the default
        // TextStyle of the theme (16).
        const Size(129.0, 14.0),
      );
    },
  );

  testWidgets(
    'strut TextStyle increases height',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: const Material(
            child: Center(
              child: SelectableText(
                'something',
                style: TextStyle(fontSize: 20),
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(SelectableText)),
        // Strut should inherit the TextStyle.fontSize by default and produce the
        // same height as if it were disabled.
        const Size(183.0, 20.0),
      );

      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: const Material(
            child: Center(
              child: SelectableText(
                'something',
                style: TextStyle(fontSize: 20),
                strutStyle: StrutStyle.disabled,
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(SelectableText)),
        // The height here should match the previous version with strut enabled.
        const Size(183.0, 20.0),
      );
    },
  );

  testWidgets(
    'strut basic multi line',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: const Material(
            child: Center(
              child: SelectableText(
                'something',
                maxLines: 6,
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(SelectableText)),
        const Size(129.0, 84.0),
      );
    },
  );

  testWidgets(
    'strut no force small strut',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: const Material(
            child: Center(
              child: SelectableText(
                'something',
                maxLines: 6,
                strutStyle: StrutStyle(
                  // The small strut is overtaken by the larger
                  // TextStyle fontSize.
                  fontSize: 5,
                ),
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(SelectableText)),
        // When the strut's height is smaller than TextStyle's and forceStrutHeight
        // is disabled, then the TextStyle takes precedence. Should be the same height
        // as 'strut basic multi line'.
        const Size(129.0, 84.0),
      );
    },
  );

  testWidgets(
    'strut no force large strut',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: const Material(
            child: Center(
              child: SelectableText(
                'something',
                maxLines: 6,
                strutStyle: StrutStyle(
                  fontSize: 25,
                ),
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(SelectableText)),
        // When the strut's height is larger than TextStyle's and forceStrutHeight
        // is disabled, then the StrutStyle takes precedence.
        const Size(129.0, 150.0),
      );
    },
  );

  testWidgets(
    'strut height override',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: const Material(
            child: Center(
              child: SelectableText(
                'something',
                maxLines: 3,
                strutStyle: StrutStyle(
                  fontSize: 8,
                  forceStrutHeight: true,
                ),
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(SelectableText)),
        // The smaller font size of strut make the field shorter than normal.
        const Size(129.0, 24.0),
      );
    },
  );

  testWidgets(
    'strut forces field taller',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: const Material(
            child: Center(
              child: SelectableText(
                'something',
                maxLines: 3,
                style: TextStyle(fontSize: 10),
                strutStyle: StrutStyle(
                  fontSize: 18,
                  forceStrutHeight: true,
                ),
              ),
            ),
          ),
        ),
      );

      expect(
        tester.getSize(find.byType(SelectableText)),
        // When the strut fontSize is larger than a provided TextStyle, the
        // strut's height takes precedence.
        const Size(93.0, 54.0),
      );
    },
  );

  testWidgets('Caret center position', (WidgetTester tester) async {
    await tester.pumpWidget(
      overlay(
        child: const SizedBox(
          width: 300.0,
          child: SelectableText(
            'abcd',
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );

    final RenderEditable editable = findRenderEditable(tester);

    Offset topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 4)).topLeft,
    );
    expect(topLeft.dx, equals(427));

    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 3)).topLeft,
    );
    expect(topLeft.dx, equals(413));

    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
    );
    expect(topLeft.dx, equals(399));

    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 1)).topLeft,
    );
    expect(topLeft.dx, equals(385));
  });

  testWidgets('Caret indexes into trailing whitespace center align', (WidgetTester tester) async {
    await tester.pumpWidget(
      overlay(
        child: const SizedBox(
          width: 300.0,
          child: SelectableText(
            'abcd    ',
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );

    final RenderEditable editable = findRenderEditable(tester);

    Offset topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 7)).topLeft,
    );
    expect(topLeft.dx, equals(469));

    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 8)).topLeft,
    );
    expect(topLeft.dx, equals(483));

    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 4)).topLeft,
    );
    expect(topLeft.dx, equals(427));

    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 3)).topLeft,
    );
    expect(topLeft.dx, equals(413));

    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
    );
    expect(topLeft.dx, equals(399));

    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 1)).topLeft,
    );
    expect(topLeft.dx, equals(385));
  });

  testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async {
    const String testText = 'lorem ipsum';
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(testText),
        ),
      ),
    );

    final EditableTextState state =
        tester.state<EditableTextState>(find.byType(EditableText));
    final RenderEditable renderEditable = state.renderEditable;

    await tester.tapAt(const Offset(20, 10));
    renderEditable.selectWord(cause: SelectionChangedCause.longPress);
    await tester.pumpAndSettle();

    final List<FadeTransition> transitions = find.descendant(
      of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
      matching: find.byType(FadeTransition),
    ).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
    expect(transitions.length, 2);
    final FadeTransition left = transitions[0];
    final FadeTransition right = transitions[1];

    expect(left.opacity.value, equals(1.0));
    expect(right.opacity.value, equals(1.0));
  });

  testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async {
    const String testText = 'lorem ipsum';

    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText(testText),
        ),
      ),
    );

    final RenderEditable renderEditable =
        tester.state<EditableTextState>(find.byType(EditableText)).renderEditable;

    await tester.tapAt(const Offset(20, 10));
    renderEditable.selectWord(cause: SelectionChangedCause.longPress);
    await tester.pumpAndSettle();

    final List<Widget> transitions =
    find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList();
    expect(transitions.length, 2);
    final FadeTransition left = transitions[0] as FadeTransition;
    final FadeTransition right = transitions[1] as FadeTransition;

    expect(left.opacity.value, equals(1.0));
    expect(right.opacity.value, equals(1.0));
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));

  testWidgets('Long press shows handles and toolbar', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText('abc def ghi'),
        ),
      ),
    );

    // Long press at 'e' in 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    await tester.longPressAt(ePos);
    await tester.pumpAndSettle();

    final EditableTextState editableText = tester.state(find.byType(EditableText));
    expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
    expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue);
  });

  testWidgets('Double tap shows handles and toolbar', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText('abc def ghi'),
        ),
      ),
    );

    // Double tap at 'e' in 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    await tester.tapAt(ePos);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(ePos);
    await tester.pump();

    final EditableTextState editableText = tester.state(find.byType(EditableText));
    expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
    expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue);
  });

  testWidgets(
    'Mouse tap does not show handles nor toolbar',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: SelectableText('abc def ghi'),
          ),
        ),
      );

      // Long press to trigger the selectable text.
      final Offset ePos = textOffsetToPosition(tester, 5);
      final TestGesture gesture = await tester.startGesture(
        ePos,
        pointer: 7,
        kind: PointerDeviceKind.mouse,
      );
      await tester.pump();
      await gesture.up();
      await tester.pump();

      final EditableTextState editableText = tester.state(find.byType(EditableText));
      expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
      expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
    },
  );

  testWidgets(
    'Mouse long press does not show handles nor toolbar',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: SelectableText('abc def ghi'),
          ),
        ),
      );

      // Long press to trigger the selectable text.
      final Offset ePos = textOffsetToPosition(tester, 5);
      final TestGesture gesture = await tester.startGesture(
        ePos,
        pointer: 7,
        kind: PointerDeviceKind.mouse,
      );
      await tester.pump(const Duration(seconds: 2));
      await gesture.up();
      await tester.pump();

      final EditableTextState editableText = tester.state(find.byType(EditableText));
      expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
      expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
    },
  );

  testWidgets(
    'Mouse double tap does not show handles nor toolbar',
    (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: SelectableText('abc def ghi'),
          ),
        ),
      );

      // Double tap to trigger the selectable text.
      final Offset selectableTextPos = tester.getCenter(find.byType(SelectableText));
      final TestGesture gesture = await tester.startGesture(
        selectableTextPos,
        pointer: 7,
        kind: PointerDeviceKind.mouse,
      );
      await tester.pump(const Duration(milliseconds: 50));
      await gesture.up();
      await tester.pump();
      await gesture.down(selectableTextPos);
      await tester.pump();
      await gesture.up();
      await tester.pump();

      final EditableTextState editableText = tester.state(find.byType(EditableText));
      expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
      expect(editableText.selectionOverlay!.handlesAreVisible, isFalse);
    },
  );

  testWidgets('text span with tap gesture recognizer works in selectable rich text', (WidgetTester tester) async {
    int spyTaps = 0;
    final TapGestureRecognizer spyRecognizer = TapGestureRecognizer()
      ..onTap = () {
        spyTaps += 1;
      };
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText.rich(
              TextSpan(
                children: <TextSpan>[
                  const TextSpan(text: 'Atwater '),
                  TextSpan(text: 'Peel', recognizer: spyRecognizer),
                  const TextSpan(text: ' Sherbrooke Bonaventure'),
                ],
              ),
            ),
          ),
        ),
      ),
    );
    expect(spyTaps, 0);
    final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

    await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
    expect(spyTaps, 1);

    // Waits for a while to avoid double taps.
    await tester.pump(const Duration(seconds: 1));

    // Starts a long press.
    final TestGesture gesture =
      await tester.startGesture(selectableTextStart + const Offset(150.0, 5.0));
    await tester.pump(const Duration(milliseconds: 500));
    await gesture.up();
    await tester.pump();
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);

    final TextEditingController controller = editableTextWidget.controller;
    // Long press still triggers selection.
    expect(
      controller.selection,
      const TextSelection(baseOffset: 8, extentOffset: 12),
    );
    // Long press does not trigger gesture recognizer.
    expect(spyTaps, 1);
  });

  testWidgets('text span with long press gesture recognizer works in selectable rich text', (WidgetTester tester) async {
    int spyLongPress = 0;
    final LongPressGestureRecognizer spyRecognizer = LongPressGestureRecognizer()
      ..onLongPress = () {
        spyLongPress += 1;
      };
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText.rich(
              TextSpan(
                children: <TextSpan>[
                  const TextSpan(text: 'Atwater '),
                  TextSpan(text: 'Peel', recognizer: spyRecognizer),
                  const TextSpan(text: ' Sherbrooke Bonaventure'),
                ],
              ),
            ),
          ),
        ),
      ),
    );
    expect(spyLongPress, 0);
    final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));

    await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
    expect(spyLongPress, 0);

    // Waits for a while to avoid double taps.
    await tester.pump(const Duration(seconds: 1));

    // Starts a long press.
    final TestGesture gesture =
    await tester.startGesture(selectableTextStart + const Offset(150.0, 5.0));
    await tester.pump(const Duration(milliseconds: 500));
    await gesture.up();
    await tester.pump();
    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);

    final TextEditingController controller = editableTextWidget.controller;
    // Long press does not trigger selection if there is text span with long
    // press recognizer.
    expect(
      controller.selection,
      const TextSelection(baseOffset: 11, extentOffset: 11, affinity: TextAffinity.upstream),
    );
    // Long press triggers gesture recognizer.
    expect(spyLongPress, 1);
  });

  testWidgets('SelectableText changes mouse cursor when hovered', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText('test'),
          ),
        ),
      ),
    );

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
    await gesture.addPointer(location: tester.getCenter(find.text('test')));

    await tester.pump();

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
  });

  testWidgets('The handles show after pressing Select All', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText('abc def ghi'),
        ),
      ),
    );

    // Long press at 'e' in 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    await tester.longPressAt(ePos);
    await tester.pumpAndSettle();

    expect(find.text('Select all'), findsOneWidget);
    expect(find.text('Copy'), findsOneWidget);
    expect(find.text('Paste'), findsNothing);
    expect(find.text('Cut'), findsNothing);
    EditableTextState editableText = tester.state(find.byType(EditableText));
    expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
    expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue);

    await tester.tap(find.text('Select all'));
    await tester.pump();
    expect(find.text('Copy'), findsOneWidget);
    expect(find.text('Select all'), findsNothing);
    expect(find.text('Paste'), findsNothing);
    expect(find.text('Cut'), findsNothing);
    editableText = tester.state(find.byType(EditableText));
    expect(editableText.selectionOverlay!.handlesAreVisible, isTrue);
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{
      TargetPlatform.android,
      TargetPlatform.fuchsia,
    }),
  );

  testWidgets('The Select All calls on selection changed', (WidgetTester tester) async {
    TextSelection? newSelection;
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
              expect(newSelection, isNull);
              newSelection = selection;
            },
          ),
        ),
      ),
    );

    // Long press at 'e' in 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    await tester.longPressAt(ePos);
    await tester.pumpAndSettle();

    expect(newSelection!.baseOffset, 4);
    expect(newSelection!.extentOffset, 7);
    newSelection = null;

    await tester.tap(find.text('Select all'));
    await tester.pump();
    expect(newSelection!.baseOffset, 0);
    expect(newSelection!.extentOffset, 11);
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{
      TargetPlatform.android,
      TargetPlatform.fuchsia,
    }),
  );

  testWidgets('The Select All calls on selection changed with a mouse on windows and linux', (WidgetTester tester) async {
    const String string = 'abc def ghi';
    TextSelection? newSelection;
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: SelectableText(
            string,
            onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
              expect(newSelection, isNull);
              newSelection = selection;
            },
          ),
        ),
      ),
    );

    // Right-click on the 'e' in 'def'.
    final Offset ePos = textOffsetToPosition(tester, 5);
    final TestGesture gesture = await tester.startGesture(
      ePos,
      kind: PointerDeviceKind.mouse,
      buttons: kSecondaryMouseButton,
    );
    await tester.pump();
    await gesture.up();
    await tester.pumpAndSettle();
    expect(newSelection!.isCollapsed, isTrue);
    expect(newSelection!.baseOffset, 5);
    newSelection = null;

    await tester.tap(find.text('Select all'));
    await tester.pump();
    expect(newSelection!.baseOffset, 0);
    expect(newSelection!.extentOffset, 11);
  },
    variant: const TargetPlatformVariant(<TargetPlatform>{
      TargetPlatform.windows,
      TargetPlatform.linux,
    }),
  );

  testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: SelectableText('abc def ghi'),
        ),
      ),
    );

    // Interact with the selectable text to establish the input connection.
    final Offset topLeft = tester.getTopLeft(find.byType(EditableText));
    final TestGesture gesture = await tester.startGesture(
      topLeft + const Offset(0.0, 5.0),
      kind: PointerDeviceKind.mouse,
    );
    await tester.pump(const Duration(milliseconds: 50));
    await gesture.up();
    await tester.pumpAndSettle();

    final EditableTextState state = tester.state(find.byType(EditableText));
    expect(state.selectionOverlay!.handlesAreVisible, isFalse);
    expect(
      state.currentTextEditingValue.selection,
      const TextSelection.collapsed(offset: 0),
    );

    if (kIsWeb) {
      tester.testTextInput.updateEditingValue(const TextEditingValue(
        selection: TextSelection(baseOffset: 2, extentOffset: 7),
      ));
      // Wait for all the `setState` calls to be flushed.
      await tester.pumpAndSettle();
      expect(
        state.currentTextEditingValue.selection,
        const TextSelection(baseOffset: 2, extentOffset: 7),
      );
      expect(state.selectionOverlay!.handlesAreVisible, isFalse);
    }
  });

  testWidgets('onSelectionChanged is called when selection changes', (WidgetTester tester) async {
    int onSelectionChangedCallCount = 0;

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: SelectableText(
            'abc def ghi',
            onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
              onSelectionChangedCallCount += 1;
            },
          ),
        ),
      ),
    );

    // Long press to select 'abc'.
    final Offset aLocation = textOffsetToPosition(tester, 1);
    await tester.longPressAt(aLocation);
    await tester.pump();
    expect(onSelectionChangedCallCount, equals(1));
    // Long press to select 'def'.
    await tester.longPressAt(textOffsetToPosition(tester, 5));
    await tester.pump();
    expect(onSelectionChangedCallCount, equals(2));
    // Tap on 'Select all' option to select the whole text.
    await tester.tap(find.text('Select all'));
    expect(onSelectionChangedCallCount, equals(3));
  });

  testWidgets('selecting a space selects the previous word on mobile', (WidgetTester tester) async {
    TextSelection? selection;

    await tester.pumpWidget(
      MaterialApp(
        home: SelectableText(
          ' blah blah',
          onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? cause) {
            selection = newSelection;
          },
        ),
      ),
    );

    expect(selection, isNull);

    // Put the cursor at the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, 10));
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 10);
    expect(selection!.extentOffset, 10);

    // Long press on the second space and the previous word is selected.
    await tester.longPressAt(textOffsetToPosition(tester, 5));
    await tester.pumpAndSettle();
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 1);
    expect(selection!.extentOffset, 5);

    // Put the cursor at the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, 10));
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 10);
    expect(selection!.extentOffset, 10);

    // Long press on the first space and the space is selected because there is
    // no previous word.
    await tester.longPressAt(textOffsetToPosition(tester, 0));
    await tester.pumpAndSettle();
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 0);
    expect(selection!.extentOffset, 1);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));

  testWidgets('selecting a space selects the space on non-mobile platforms', (WidgetTester tester) async {
    TextSelection? selection;

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText(
              ' blah blah',
              onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? cause) {
                selection = newSelection;
              },
            ),
          ),
        ),
      ),
    );

    expect(selection, isNull);

    // Put the cursor at the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, 10));
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 10);
    expect(selection!.extentOffset, 10);

    // Double tapping the second space selects it.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 5));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 5));
    await tester.pumpAndSettle();
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 5);
    expect(selection!.extentOffset, 6);

    // Tap at the beginning of the text to hide the toolbar, then at the end to
    // move the cursor to the end. On some platforms, the context menu would
    // otherwise block a tap on the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pumpAndSettle();
    await tester.tapAt(textOffsetToPosition(tester, 10));
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 10);
    expect(selection!.extentOffset, 10);

    // Double tapping the first space selects it.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pumpAndSettle();
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 0);
    expect(selection!.extentOffset, 1);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS,  TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }));

  testWidgets('double tapping a space selects the previous word on mobile', (WidgetTester tester) async {
    TextSelection? selection;

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Center(
            child: SelectableText(
              ' blah blah  \n  blah',
              onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? cause) {
                selection = newSelection;
              },
            ),
          ),
        ),
      ),
    );

    expect(selection, isNull);

    // Put the cursor at the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, 19));
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 19);
    expect(selection!.extentOffset, 19);

    // Double tapping the second space selects the previous word.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 5));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 5));
    await tester.pumpAndSettle();
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 1);
    expect(selection!.extentOffset, 5);

    // Double tapping does the same thing for the first space.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pumpAndSettle();
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 0);
    expect(selection!.extentOffset, 1);

    // Put the cursor at the end of the field.
    await tester.tapAt(textOffsetToPosition(tester, 19));
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 19);
    expect(selection!.extentOffset, 19);

    // Double tapping the last space selects all previous contiguous spaces on
    // both lines and the previous word.
    await tester.pump(const Duration(milliseconds: 500));
    await tester.tapAt(textOffsetToPosition(tester, 14));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 14));
    await tester.pumpAndSettle();
    expect(selection, isNotNull);
    expect(selection!.baseOffset, 6);
    expect(selection!.extentOffset, 14);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));

  testWidgets('text selection style 1', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Center(
            child: Column(
              children: const <Widget>[
                SelectableText.rich(
                  TextSpan(
                    children: <TextSpan>[
                      TextSpan(
                        text: 'Atwater Peel ',
                        style: TextStyle(
                          fontSize: 30.0,
                        ),
                      ),
                      TextSpan(
                        text: 'Sherbrooke Bonaventure ',
                        style: TextStyle(
                          fontSize: 15.0,
                        ),
                      ),
                      TextSpan(
                        text: 'hi wassup!',
                        style: TextStyle(
                          fontSize: 10.0,
                        ),
                      ),
                    ],
                  ),
                  key: Key('field0'),
                  selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingTop,
                  selectionWidthStyle: ui.BoxWidthStyle.max,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;
    controller.selection = const TextSelection(baseOffset: 0, extentOffset: 46);
    await tester.pump();

    await expectLater(
      find.byType(MaterialApp),
      matchesGoldenFile('selectable_text_golden.TextSelectionStyle.1.png'),
    );
  });

  testWidgets('text selection style 2', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Center(
            child: Column(
              children: const <Widget>[
                SelectableText.rich(
                  TextSpan(
                    children: <TextSpan>[
                      TextSpan(
                        text: 'Atwater Peel ',
                        style: TextStyle(
                          fontSize: 30.0,
                        ),
                      ),
                      TextSpan(
                        text: 'Sherbrooke Bonaventure ',
                        style: TextStyle(
                          fontSize: 15.0,
                        ),
                      ),
                      TextSpan(
                        text: 'hi wassup!',
                        style: TextStyle(
                          fontSize: 10.0,
                        ),
                      ),
                    ],
                  ),
                  key: Key('field0'),
                  selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingBottom,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
    final TextEditingController controller = editableTextWidget.controller;
    controller.selection = const TextSelection(baseOffset: 0, extentOffset: 46);
    await tester.pump();

    await expectLater(
      find.byType(MaterialApp),
      matchesGoldenFile('selectable_text_golden.TextSelectionStyle.2.png'),
    );
  });

  testWidgets('keeps alive when has focus', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: DefaultTabController(
          length: 2,
          child: Scaffold(
            body: NestedScrollView(
              headerSliverBuilder:
                (BuildContext context, bool innerBoxIsScrolled) {
                  return <Widget>[
                    SliverToBoxAdapter(
                      child: Container(
                        height: 200,
                        color: Colors.black12,
                        child: const Center(child: Text('Sliver 1')),
                      ),
                    ),
                    const SliverToBoxAdapter(
                      child: Center(
                        child: TabBar(
                          labelColor: Colors.black,
                          tabs: <Tab>[
                            Tab(text: 'Sliver Tab 1'),
                            Tab(text: 'Sliver Tab 2'),
                          ],
                        ),
                      )
                    ),
                  ];
                },
                body: const TabBarView(
                  children: <Widget>[
                    Padding(
                      padding: EdgeInsets.only(top: 100.0),
                      child: Text('Regular Text'),
                    ),
                    Padding(
                      padding: EdgeInsets.only(top: 100.0),
                      child: SelectableText('Selectable Text'),
                    ),
                  ],
                ),
            ),
          ),
        ),
      ),
    );

    // Without any selection, the offscreen widget is disposed and can't be
    // found, for both Text and SelectableText.
    expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
    expect(find.byType(SelectableText, skipOffstage: false), findsNothing);

    await tester.tap(find.text('Sliver Tab 2'));
    await tester.pumpAndSettle();
    expect(find.text('Regular Text', skipOffstage: false), findsNothing);
    expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);

    await tester.tap(find.text('Sliver Tab 1'));
    await tester.pumpAndSettle();
    expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
    expect(find.byType(SelectableText, skipOffstage: false), findsNothing);

    // Switch back to tab 2 and select some text in SelectableText.
    await tester.tap(find.text('Sliver Tab 2'));
    await tester.pumpAndSettle();
    expect(find.text('Regular Text', skipOffstage: false), findsNothing);
    expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);

    final EditableText editableText = tester.widget(find.byType(EditableText));
    expect(editableText.controller.selection.isValid, isFalse);
    await tester.tapAt(textOffsetToPosition(tester, 4));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(textOffsetToPosition(tester, 4));
    await tester.pumpAndSettle();
    expect(editableText.controller.selection.isValid, isTrue);
    expect(editableText.controller.selection.baseOffset, 0);
    expect(editableText.controller.selection.extentOffset, 'Selectable'.length);

    // Switch back to tab 1. The SelectableText remains because it is preserving
    // its selection.
    await tester.tap(find.text('Sliver Tab 1'));
    await tester.pumpAndSettle();
    expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
    expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);
  });

  group('magnifier', () {
    late ValueNotifier<MagnifierInfo> magnifierInfo;
    final Widget fakeMagnifier = Container(key: UniqueKey());

    testWidgets(
        'Can drag handles to show, unshow, and update magnifier',
        (WidgetTester tester) async {
      const String testValue = 'abc def ghi';
      final SelectableText selectableText = SelectableText(
            testValue,
            magnifierConfiguration: TextMagnifierConfiguration(
        magnifierBuilder: (
          _,
          MagnifierController controller,
          ValueNotifier<MagnifierInfo> localMagnifierInfo
        ) {
          magnifierInfo = localMagnifierInfo;
          return fakeMagnifier;
        },
      )
          );

      await tester.pumpWidget(
        overlay(
          child: selectableText,
        ),
      );

      await skipPastScrollingAnimation(tester);

      // Double tap the 'e' to select 'def'.
      await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pump(const Duration(milliseconds: 30));
      await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
      await tester.pump(const Duration(milliseconds: 30));

      final TextSelection selection = TextSelection(
        baseOffset: testValue.indexOf('d'),
        extentOffset: testValue.indexOf('f')
      );

      final RenderEditable renderEditable = findRenderEditable(tester);
      final List<TextSelectionPoint> endpoints = globalize(
        renderEditable.getEndpointsForSelection(selection),
        renderEditable,
      );

      // Drag the right handle 2 letters to the right.
      final Offset handlePos = endpoints.last.point + const Offset(1.0, 1.0);
      final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);

      Offset? firstDragGesturePosition;

      await gesture.moveTo(textOffsetToPosition(tester, testValue.length - 2));
      await tester.pump();

      expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
      firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;

      await gesture.moveTo(textOffsetToPosition(tester, testValue.length));
      await tester.pump();

      // Expect the position the magnifier gets to have moved.
      expect(firstDragGesturePosition,
          isNot(magnifierInfo.value.globalGesturePosition));

      await gesture.up();
      await tester.pump();

      expect(find.byKey(fakeMagnifier.key!), findsNothing);
    });
  });

  testWidgets('SelectableText text span style is merged with default text style', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/71389

    const TextStyle textStyle = TextStyle(color: Color(0xff00ff00), fontSize: 12.0);

    await tester.pumpWidget(
      const MaterialApp(
        home: SelectableText.rich(
          TextSpan(
            text: 'Abcd',
            style: textStyle,
          ),
        ),
      ),
    );

    final EditableText editableText = tester.widget(find.byType(EditableText));
    expect(editableText.style.fontSize, textStyle.fontSize);
  });
}
